/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.beepmystuff;

import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reportMatcher;
import static org.easymock.EasyMock.verify;

import com.google.beepmystuff.BmsClient.AddEanResult;
import com.google.beepmystuff.BmsClient.LoginResult;
import com.google.beepmystuff.BmsClient.ShareWith;
import com.google.beepmystuff.DefaultBmsLowLevelClient.BmsRawHttpApiClient;

import junit.framework.TestCase;

import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.easymock.IArgumentMatcher;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import java.util.Collections;

public class DefaultBmsLowLevelClientTest extends TestCase {
  private static final String TEST_KEY = "squeamish_ossifrage";
  private BmsHttpClient mockHttpClient;
  private BmsLowLevelClient client;

  @Override
  protected void setUp() throws Exception {
    mockHttpClient = createMock(BmsHttpClient.class);
    client = new DefaultBmsLowLevelClient(mockHttpClient, TEST_KEY);
  }

  public void testShouldLogin() throws Exception {
    HttpUriRequest expectedRequest = new HttpGet("http://www.beepmystuff.com/api/login"
        + "?apikey=squeamish_ossifrage&username=me&password=secret%2F");
    String response = "<result><info><version>1.0</version>"
        + "<request>http://www.beepmystuff.com/api/login?username=me&amp;"
        + "apikey=sqeamish_ossifrage&amp;password=secret%2F</request>"
        + "<key>squeamish_ossifrage</key></info>"
        + "<login><session>abcd1234</session></login>"
        + "<error><errorcode>0</errorcode><string>Success</string></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    LoginResult result = client.login("me", "secret/");
    assertEquals("abcd1234", result.getSessionToken());
    verify(mockHttpClient);
  }

  public void testShouldReportInvalidCredentialsDuringLogin() throws Exception {
    HttpUriRequest expectedRequest = new HttpGet("http://www.beepmystuff.com/api/login"
        + "?apikey=squeamish_ossifrage&username=me&password=secret%2F");
    String response = "<result><info><version>1.0</version>"
        + "<request>http://www.beepmystuff.com/api/login?username=me&amp;"
        + "apikey=sqeamish_ossifrage&amp;password=secret%2F</request>"
        + "<key>squeamish_ossifrage</key></info>"
        + "<login><session>abcd1234</session></login>"
        + "<error><errorcode>-2</errorcode>"
        + "<string>Invalid username or password</string></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    try {
      client.login("me", "secret/");
      fail("Expected BmsApiException");
    } catch (BmsApiException e) {
      assertEquals(BmsApiException.ErrorCode.INVALID_USERNAME, e.getErrorCode());
      assertEquals("Invalid username or password", e.getMessage());
    }
    verify(mockHttpClient);
  }

  public void testShouldGetAvatarPath() throws Exception {
    HttpUriRequest expectedRequest = new HttpGet("http://www.beepmystuff.com/api/getavatarpath"
        + "?apikey=squeamish_ossifrage&session=abcd1234");
    String response = "<result><info><version>1.0</version>"
        + "<request>http://www.beepmystuff.com/api/getavatarpath?"
        + "apikey=squeamish_ossifrage&amp;session=abcd1234</request>"
        + "<key>squeamish_ossifrage</key></info>"
        + "<avatar><path>http://www.beepmystuff.com/static/images/avatar.png</path></avatar>"
        + "<error><errorcode>0</errorcode><string>Success</string></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    String result = client.getAvatarPath("abcd1234");
    assertEquals("http://www.beepmystuff.com/static/images/avatar.png", result);
    verify(mockHttpClient);
  }

  public void testShouldReportSessionTokenExpiry() throws Exception {
    // Using a getavatarpath request as an example of a call that might fail.
    HttpUriRequest expectedRequest = new HttpGet("http://www.beepmystuff.com/api/getavatarpath"
        + "?apikey=squeamish_ossifrage&session=expired");
    String response = "<result><info><version>1.0</version>"
        + "<request>http://www.beepmystuff.com/api/getavatarpath?"
        + "apikey=squeamish_ossifrage&amp;session=expired</request>"
        + "<key>squeamish_ossifrage</key></info>"
        + "<error><errorcode>-3</errorcode><string>Session has expired</string></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    try {
      client.getAvatarPath("expired");
      fail("Expected BmsApiException");
    } catch (BmsApiException e) {
      assertEquals(BmsApiException.ErrorCode.INVALID_SESSION, e.getErrorCode());
      assertEquals("Session has expired", e.getMessage());
    }
    verify(mockHttpClient);
  }

  public void testShouldAddEanSharedWithEveryone() throws Exception {
    HttpUriRequest expectedRequest = new HttpPost("http://www.beepmystuff.com/api/addean"
        + "?apikey=squeamish_ossifrage&session=abcd1234&ean=9780321356680&public=true&share=true");
    String response = "<result><info><version>1.0</version>"
        + "<request>http://www.beepmystuff.com/api/addean?"
        + "apikey=squeamish_ossifrage&amp;session=abcd1234&amp;share=true&amp;public=true"
        + "&amp;ean=9780321356680</request>"
        + "<key>squeamish_ossifrage</key></info>"
        + "<addean><msg>Your item has been added</msg><error_msg></error_msg>"
        + "<imgurl>http://example.com/images/418.jpg</imgurl>"
        + "<title>Book</title></addean>"
        + "<error><errorcode>0</errorcode><string>Success</string></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    AddEanResult result = client.addEan("abcd1234", "9780321356680", ShareWith.PUBLIC);
    assertEquals("Your item has been added", result.getMessage());
    assertEquals("", result.getErrorMessage());
    assertEquals("http://example.com/images/418.jpg", result.getImageUrl());
    assertEquals("Book", result.getTitle());
    verify(mockHttpClient);
  }

  public void testShouldAddEanSharedWithFriends() throws Exception {
    HttpUriRequest expectedRequest = new HttpPost("http://www.beepmystuff.com/api/addean"
        + "?apikey=squeamish_ossifrage&session=abcd1234&ean=9780321356680&public=false&share=true");
    String response = "<result><info><version>1.0</version>"
        + "<request>http://www.beepmystuff.com/api/addean?"
        + "apikey=squeamish_ossifrage&amp;session=abcd1234&amp;share=true&amp;public=false"
        + "&amp;ean=9780321356680</request>"
        + "<key>squeamish_ossifrage</key></info>"
        + "<addean><msg>Your item has been added</msg><error_msg></error_msg>"
        + "<imgurl>http://example.com/images/418.jpg</imgurl>"
        + "<title>Book</title></addean>"
        + "<error><errorcode>0</errorcode><string>Success</string></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    AddEanResult result = client.addEan("abcd1234", "9780321356680", ShareWith.FRIENDS);
    assertEquals("Your item has been added", result.getMessage());
    assertEquals("", result.getErrorMessage());
    assertEquals("http://example.com/images/418.jpg", result.getImageUrl());
    assertEquals("Book", result.getTitle());
    verify(mockHttpClient);
  }

  public void testShouldAddEanSharedWithNobody() throws Exception {
    HttpUriRequest expectedRequest = new HttpPost("http://www.beepmystuff.com/api/addean"
        + "?apikey=squeamish_ossifrage&session=abcd1234&ean=9780321356680&public=false&share=false");
    String response = "<result><info><version>1.0</version>"
        + "<request>http://www.beepmystuff.com/api/addean?"
        + "apikey=squeamish_ossifrage&amp;session=abcd1234&amp;share=false&amp;public=false"
        + "&amp;ean=9780321356680</request>"
        + "<key>squeamish_ossifrage</key></info>"
        + "<addean><msg>Your item has been added</msg><error_msg></error_msg>"
        + "<imgurl>http://example.com/images/418.jpg</imgurl>"
        + "<title>Book</title></addean>"
        + "<error><errorcode>0</errorcode><string>Success</string></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    AddEanResult result = client.addEan("abcd1234", "9780321356680", ShareWith.PRIVATE);
    assertEquals("Your item has been added", result.getMessage());
    assertEquals("", result.getErrorMessage());
    assertEquals("http://example.com/images/418.jpg", result.getImageUrl());
    assertEquals("Book", result.getTitle());
    verify(mockHttpClient);
  }

  public void testShouldExtractTextFromXml() throws Exception {
    BmsHttpClient dummyClient = createMock(BmsHttpClient.class);
    replay(dummyClient);
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(dummyClient);
    String response = "<result><info><version>1.0</version><request>url</request></info></result>";
    Document document = rawClient.parse(response);
    assertEquals("url", BmsRawHttpApiClient.getTextContent(document, "info", "request"));
  }

  public void testShouldFailToExtractTextIfNonUnique() throws Exception {
    BmsHttpClient dummyClient = createMock(BmsHttpClient.class);
    replay(dummyClient);
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(dummyClient);
    String response = "<result><info><request>url</request>"
        + "<version>1.0</version><request>url</request></info></result>";
    Document document = rawClient.parse(response);
    try {
      BmsRawHttpApiClient.getTextContent(document, "info", "request");
      fail("Expected BmsClientException");
    } catch(BmsClientException e) {
      assertEquals("Document has more than one 'request' child in path [info, request]",
          e.getMessage());
    }
  }

  public void testShouldFailToExtractTextIfNotFound() throws Exception {
    BmsHttpClient dummyClient = createMock(BmsHttpClient.class);
    replay(dummyClient);
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(dummyClient);
    String response = "<result><info><request>url</request>"
        + "<version>1.0</version><request>url</request></info></result>";
    Document document = rawClient.parse(response);
    try {
      BmsRawHttpApiClient.getTextContent(document, "notfound", "request");
      fail("Expected BmsClientException");
    } catch(BmsClientException e) {
      assertEquals("Document has no 'notfound' child in path [notfound, request]",
          e.getMessage());
    }
  }

  public void testShouldFailToExtractTextIfMixedContent() throws Exception {
    BmsHttpClient dummyClient = createMock(BmsHttpClient.class);
    replay(dummyClient);
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(dummyClient);
    String response = "<result><info>some info here"
        + "<version>1.0</version><request>url</request></info></result>";
    Document document = rawClient.parse(response);
    try {
      BmsRawHttpApiClient.getTextContent(document, "info");
      fail("Expected BmsClientException");
    } catch(BmsClientException e) {
      assertEquals("Mixed-content element (type 1) at path [info]", e.getMessage());
    }
  }

  public void testShouldExtractTextIgnoringComments() throws Exception {
    BmsHttpClient dummyClient = createMock(BmsHttpClient.class);
    replay(dummyClient);
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(dummyClient);
    // Technically, I think this _is_ mixed content too, but we know what it
    // means.
    String response = "<result><info><version><!-- some comments here -->"
        + "1.0<!-- and here --> at least</version></info></result>";
    Document document = rawClient.parse(response);
    assertEquals("1.0 at least",
        BmsRawHttpApiClient.getTextContent(document, "info", "version"));
  }

  public void testShouldExtractTextCombiningTextAndCData() throws Exception {
    BmsHttpClient dummyClient = createMock(BmsHttpClient.class);
    replay(dummyClient);
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(dummyClient);
    String response = "<result><info><version attr=\"attr\">1.0<![CDATA[ (<>) ]]> 2.0"
        + "</version></info></result>";
    Document document = rawClient.parse(response);
    assertEquals("1.0 (<>)  2.0",
        BmsRawHttpApiClient.getTextContent(document, "info", "version"));
  }

  public void testShouldExtractTextFromEmptyElement() throws Exception {
    BmsHttpClient dummyClient = createMock(BmsHttpClient.class);
    replay(dummyClient);
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(dummyClient);
    String response = "<result><info><version/></info></result>";
    Document document = rawClient.parse(response);
    assertEquals("",
        BmsRawHttpApiClient.getTextContent(document, "info", "version"));
  }

  public void testShouldParseErrorDetailsFromResponse() throws Exception {
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(mockHttpClient);

    HttpUriRequest expectedRequest = new HttpGet("http://www.beepmystuff.com/api/verb");
    String response = "<result>"
        + "<error><errorcode>-3</errorcode><string>Session has expired</string></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    try {
      rawClient.makeRpc("verb", true, Collections.<String, String>emptyMap());
      fail("Expected BmsApiException");
    } catch (BmsApiException e) {
      assertEquals(BmsApiException.ErrorCode.INVALID_SESSION, e.getErrorCode());
      assertEquals("Session has expired", e.getMessage());
    }
    verify(mockHttpClient);
  }

  public void testShouldOnlyCheckErrorMessageIfError() throws Exception {
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(mockHttpClient);

    HttpUriRequest expectedRequest = new HttpGet("http://www.beepmystuff.com/api/verb");
    String response = "<result>"
        + "<error><errorcode>0</errorcode></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    assertNotNull(rawClient.makeRpc("verb", true, Collections.<String, String>emptyMap()));
    verify(mockHttpClient);
  }

  public void testShouldThrowIfErrorCodeUnparsable() throws Exception {
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(mockHttpClient);

    HttpUriRequest expectedRequest = new HttpGet("http://www.beepmystuff.com/api/verb");
    String response = "<result>"
        + "<error><errorcode>kittens</errorcode></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    try {
      rawClient.makeRpc("verb", true, Collections.<String, String>emptyMap());
      fail("Expected BmsClientException");
    } catch (BmsClientException e) {
      assertEquals("Error parsing result: errorcode 'kittens' unparsable", e.getMessage());
    }
    verify(mockHttpClient);
  }

  public void testShouldThrowIfErrorCodeUnknown() throws Exception {
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(mockHttpClient);

    HttpUriRequest expectedRequest = new HttpGet("http://www.beepmystuff.com/api/verb");
    String response = "<result>"
        + "<error><errorcode>404</errorcode></error></result>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    try {
      rawClient.makeRpc("verb", true, Collections.<String, String>emptyMap());
      fail("Expected BmsClientException");
    } catch (BmsClientException e) {
      assertEquals("Error parsing result: errorcode 404 unknown", e.getMessage());
    }
    verify(mockHttpClient);
  }

  public void testShouldThrowIfResponseUnparsable() throws Exception {
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(mockHttpClient);

    HttpUriRequest expectedRequest = new HttpGet("http://www.beepmystuff.com/api/verb");
    String response = "I'm not <xml>";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    try {
      rawClient.makeRpc("verb", true, Collections.<String, String>emptyMap());
      fail("Expected BmsClientException");
    } catch (BmsClientException e) {
      assertTrue(e.getMessage().startsWith("Error parsing result: "));
      assertTrue(e.getCause() instanceof SAXException);
    }
    verify(mockHttpClient);
  }

  public void testShouldThrowIfResponseIsText() throws Exception {
    DefaultBmsLowLevelClient.BmsRawHttpApiClient rawClient =
        new DefaultBmsLowLevelClient.BmsRawHttpApiClient(mockHttpClient);

    HttpUriRequest expectedRequest = new HttpGet("http://www.beepmystuff.com/api/verb");
    String response = "418 I'm a little teapot";
    expect(mockHttpClient.getResponseBodyAsString(eqHttpRequest(expectedRequest)))
        .andReturn(response);
    replay(mockHttpClient);

    try {
      rawClient.makeRpc("verb", true, Collections.<String, String>emptyMap());
      fail("Expected BmsClientException");
    } catch (BmsClientException e) {
      assertEquals("Error parsing result: no document element", e.getMessage());
      assertNull(e.getCause());
    }
    verify(mockHttpClient);
  }

  /** Expects an HttpRequest that is equal to the given value. */
  private static <T extends HttpRequest> T eqHttpRequest(T value) {
    reportMatcher(new HttpRequestEquals(value));
    return null;
  }

  /**
   * A matcher for HttpRequest, as the default implementations implement
   * neither equals() nor toString().
   */
  private static class HttpRequestEquals implements IArgumentMatcher {
    private final HttpRequest expected;

    public HttpRequestEquals(HttpRequest expected) {
      this.expected = expected;
    }

    @Override
    public boolean matches(Object argument) {
      if (!(argument instanceof HttpRequest)) {
        return false;
      }
      return toString(expected).equals(toString((HttpRequest) argument));
    }

    @Override
    public void appendTo(StringBuffer buffer) {
      buffer.append(toString(expected));
    }

    private static String toString(HttpRequest request) {
      StringBuilder sb = new StringBuilder();
      sb.append(request.getRequestLine());
      for (Header header : request.getAllHeaders()) {
        sb.append(String.format("\n%s: %s", header.getName(), header.getValue()));
      }
      return sb.toString();
    }
  }
}
